/**
 * Copyright (c) 2010
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * *****************************************************************
 * *****************************************************************
 *
 * @fileoverview Provides pivot table functionality to Gviz Datatable.
 *
 * This component allows for the pivoting of values held in a Gviz Datatable.
 * Values that are pivoted are summed; there are a host of aggregation options
 * available for the summed column results, restricted to:
 * sum, min, max, med, var, sdev
 * 
 * It's also possible to compute values based on the result columns,
 * allowing for sum, sub, div, mult between two columns.
 *
 * Samples have been provided below.
 *
 * @param data_table {google.visualization.DataTable} DataTable to work with
 * @param kci {Number} column index to use as key for pivoted table
 * @param pci {Number} column Index to pivot
 * @param agg_cols {Object} object to hold value column aggregation options
 *                  Values => 'agg', ['computed', 'hideCols']
 * @return {google.visualization.DataView}
 *
 * Sample Usage:
 *
 * Just pivoting:
 * pivot = PivotTable(gviz_table, 1, 0, {'agg':[{'aggType':'sum', 'valueIndex':3},
 *                                              {'aggType':'sum', 'valueIndex':4}],
 *                                      });
 *
 *
 * With hidden columns and computed values:
 * pivot = PivotTable(gviz_table, 1, 0, {'agg':[{'aggType':'sum', 'valueIndex':3},
 *						{'aggType':'sum', 'valueIndex':4}],
 *  				     	 'hideCols':[1],
 *                                       'computed':{'values':[3,4], 'operation':'*'}
 *  																		});
 * 
 */

function PivotTable(data_table, kci, pci, options) {

    this.dt = data_table.clone();
    var numrows = this.dt.getNumberOfRows();
    var key_col_i = kci;
    var pivot_col_i = pci;
    var value_col_i = [];
    var aggregation_cols = false;
    var computed_cols = false;
    var hide_cols = false;
    var settings = false;
    var new_cols = {};
    //a record of col indexes added to the table
    var new_rows = [];
    //a record of key row indexes in our table
    var keep_rows = {};
    var no_agg = [];

    /*
     * Init
     */
    this.init = function () {
        this.parseOptions(options);
        if (aggregation_cols) {
            for (var i in aggregation_cols) {
                value_col_i.push(aggregation_cols[i]['valueIndex']);
            }
            if (computed_cols) {
                settings = computed_cols;
            }
        }
    }

    /**
     *  Handles options provided by the user.
     *  
     *  @param opt {Object} of options.
     *  This can hold three types of entry as follows:
     *    'agg'
     *    'computed'
     *    'hideCols'
     */
    this.parseOptions = function (opt) {
        if (options == undefined) {
            alert("ERROR, options undefined");
        }
        aggregation_cols = (opt['agg'] != undefined) ? opt['agg'] : false;
        computed_cols = (opt['computed'] != undefined) ? opt['computed'] : false;
        hide_cols = (opt['hideCols'] != undefined) ? opt['hideCols'] : false;
    }

    /**
     *  Handles the addition of a new column to our data table.
     *
     *  All added column indexes are added to new_cols object.
     *  Structure of new_cols:
     *  {original_index: {'NewColumnTitleA': new_index,
     *                    'NewColumnTitleB': new_index,
     *                    'count': count_new_columns_added,
     *                    'indexes': [indexes, new, cols, added]}}
     *  Sample Content:
     *  {3: {Oct 17, 2010-Incoming: 5,
     *       Oct 18, 2010-Incoming: 7,
     *       count: 2,
     *       indexes: [5,7]},
     *   4: {yadda, yadda...}}
     *
     *  @param title {String} the title of the new column
     *  @param index {Number} the index of column that this column is being
     *                        generated for. Used to obtain 'type' of new column.
     *  @return The index of the newly generated column.
     **/
    this.addColumn = function (title, index) {
        if (new_cols[index] == undefined) {
            new_cols[index] = {
                'count': 0,
                'indexes': []
            };
        }
        if (new_cols[index][title] == undefined) {
            this.dt.addColumn(this.dt.getColumnType(index), title);
            new_col_index = this.dt.getNumberOfColumns() - 1;
            new_cols[index][title] = new_col_index;
            new_cols[index]['count'] += 1;
            new_cols[index]['indexes'].push(new_col_index);
        }
        return new_cols[index][title];
    }

    /*  Collects key rows.
     *
     *  Gathers a set of row indexes to present in our final data view.
     *  'Keys' will be a set of values obtained from the column specified
     *  by key_col_i. A given key will only exist once.
     *
     *  @param key {String} a value for row:i in col: key_col_i
     *  @param index {Integer} the index of row that this key represents.
     *
     *  @return The index of the row.
     */
    this.addKeyRow = function (key, index) {
        if (keep_rows[key] == undefined) {
            keep_rows[key] = index;
            new_rows.push(index);
        }
        return keep_rows[key];
    }

    /**
     *  Gets row entries for a set of columns.
     *
     *  @param row_index {Number} index of row to fetch for
     *  @param col_indexes {Array} array of columns to fetch for
     *  @returns {Array} of row entries
     */
    this.getRowEntries = function (row_index, col_indexes) {
        var r = [];
        for (var m in col_indexes) {
            r.push(this.dt.getValue(row_index, col_indexes[m]));
        }
        return r;
    }

    /**
     *  Sets new value in generated column of data table.
     *
     *  Adds a value to a column which has been pivoted. If the the column already
     *  has a value, the entries will be summed.
     *
     *  @param key_row_index {Number} row index that this value should be added to
     *  @param col_index {Number} column index that this value should be added to
     *  @param entry {Number} value to be added
     */

    this.setPivotedValue = function (key_row_index, col_index, entry) {
        // get value currently in target
        current_value = this.dt.getFormattedValue(key_row_index, col_index);
        entry_value = Number(current_value) + Number(entry);
        // update the value of the target cell
        this.dt.setValue(key_row_index, col_index, entry_value);
    }

    /**
     *  Converts value column titles to reflect aggregation.
     *
     *  Changes the title of a value column as follows:
     *    Original title: Incoming
     *    New title: Sum (Incoming)
     *  Does this for each value being pivoted
     *
     *  @param col_index {Number} index of column to set title for
     *  @param option {String} aggregation option selected
     */
    this.setAggregationColumnTitle = function (col_index, option) {
        current_title = this.dt.getColumnLabel(col_index);
        new_title = option + "(" + current_title + ")";
        this.dt.setColumnLabel(col_index, new_title);
    }

    /**
     *  Sets values in root columns based on values pivoted.
     */
    this.runAggregation = function () {
        for (n in aggregation_cols) {
            var option = aggregation_cols[n]['aggType'];
            var col_index = aggregation_cols[n]['valueIndex'];

            if (option == 'none') {
                no_agg.push(col_index);
                continue;
            }
            this.setAggregationColumnTitle(col_index, option);
            for (k = 0; k < numrows; k++) {
                entries = this.getRowEntries(k, new_cols[col_index]['indexes']);
                result = new Stats(entries);
                switch (option) {
                case 'sum':
                    r = result.sum;
                    break;
                case 'min':
                    r = result.min;
                    break;
                case 'max':
                    r = result.max;
                    break;
                case 'med':
                    r = result.median;
                    break;
                case 'var':
                    r = result.variance;
                    break;
                case 'sdev':
                    r = result.sdev;
                    break;
                }
                r = Math.round(r * 100) / 100;
                this.dt.setValue(k, value_col_i[n], r);
            }
        }
    }

    /**  
     *  Adds results to a computed metric column.
     *
     *  Adds a computed metric column to present some
     *  basic computed values, based on aggregated column totals.
     *
     *  This functionality is available if the user has selected
     *  to pivot two values _only_.
     *
     *  @param settings {String} represents the operation that a user
     *                         wishes to perform on the aggregated metrics.
     *        Options: '/', '*', '+', '-'
     *
     */
    this.addComputedMetric = function (settings) {
        if (settings['columnTitle'] == undefined) {
            column_title = 'computed: ' + 'a' + settings['operation'] + 'b';
        } else {
            column_title = settings['columnTitle'];
        }
        this.dt.addColumn('number', column_title);
        var new_col_index = dt.getNumberOfColumns() - 1;
        for (i = 0; i < numrows; i++) {
            result = this._performCalc(i, settings);
            if (isNaN(result)) {
                result = 0;
            }
            this.dt.setValue(i, new_col_index, result);
        }
    }

    /**
     *  Get indexes of columns to hide.
     *
     *  @hideColsArray {Array<Number>} array of root column indexes to hide
     *                                 values for.
     *  @returns {Array<Number>} array of column indexes to hide
     */
    this.getColumnsToHide = function (hideColsArray) {
        var arr = [];
        for (i in hideColsArray) {
            arr.push(hideColsArray[i]);
            var index = hideColsArray[i];
            if (new_cols[index] != undefined) {
                var indexes = new_cols[index]['indexes'];
                for (x in indexes) {
                    arr.push(indexes[x]);
                }
            }
        }
        return arr;
    }

    /**
     *  Calculates results for computed metrics.
     *
     *  Performs operation based on the two columns that
     *  values have been pivoted from.
     *  Note: Calculation will be based on the order that
     *    a user specifies in the values_cols_i array.
     *    i.e.: [a, b] = A * B
     *          [3, 4] = 3 * 4
     *
     *  @param row_i {Number} row index to perform operation on
     *  @param settings {String} represents the operation that a user
     *                         wishes to perform on the aggregated metrics.
     *                         Options: '/', '*', '+', '-'
     *
     *  @returns {Number} computed result rounded to two decimal places
     */
    this._performCalc = function (row_i, settings) {
        dp = 100;
        row_i = Number(row_i);
        a_value_index = settings['values'][0];
        b_value_index = settings['values'][1];
        operator = settings['operation'];
        a = this.dt.getValue(row_i, a_value_index);
        b = this.dt.getValue(row_i, b_value_index);
        if (operator == '*') {
            return Math.round(a * b * dp) / dp;
        }
        if (operator == '/') {
            return Math.round((a / b) * dp) / dp;
        }
        if (operator == '+') {
            return Math.round((a + b) * dp) / dp;
        }
        if (operator == '-') {
            return Math.round((a - b) * dp) / dp;
        }
    }


    /** 
     *  Main worker.
     *
     *  Loops through each row in our base data table.
     *  For each row, grabs the value(s) we want pivoted.
     *  If a pivoted column exists, adds to the correct row,
     *  otherwise, generates the column, then adds to the row.
     *  The 'correct' row is the 'key row', and will be the first
     *  instance of our key in the data table.
     *
     *  Finally:
     *    sets friendlier titles for agg. columns
     *    adds computed metric column, if specified by user
     *    removes the column that we were pivoting on
     *
     *  @returns {DataTable} Gviz DataTable object, pivoted to the max.
     */
    this.run = function () {
        for (i = 0; i < numrows; i++) {
            for (k in value_col_i) {
                var row_index = i;
                var date = this.dt.getFormattedValue(row_index, pivot_col_i);
                var value_col_label = this.dt.getColumnLabel(value_col_i[k]);
                var col_index = this.addColumn(date + "-" + value_col_label, value_col_i[k]);
                var entry = this.dt.getFormattedValue(row_index, value_col_i[k]);
                var key = this.dt.getFormattedValue(row_index, key_col_i);
                var key_row_index = this.addKeyRow(key, row_index);
                this.setPivotedValue(key_row_index, col_index, entry);
            }
        }

        if (aggregation_cols) {
            this.runAggregation();
        }
        if (computed_cols) {
            this.addComputedMetric(computed_cols);
        }
        // remove the column we pivoted on
        return this.dt;
    }

    this.init();
    pivoted_table = this.run();
    // it is neccessary to convert this to a DataView so that we can
    // easily filter to key rows only, ensuring duplicate rows
    // do not exist. Return the above pivoted_table if you want to
    // see the actual DataTable generated.
    // any other datatable manipulation (aside from standard dataView stuff)
    // needs to be done before this conversion!
    var view = new google.visualization.DataView(pivoted_table);
    view.setRows(new_rows);
    var hide_cols = this.getColumnsToHide(hide_cols).concat(no_agg);
    view.hideColumns(hide_cols);
    view.hideColumns([pivot_col_i]);
    return view;
}

/** 
 * Code copied from:
 * http://maiaco.com/articles/computingStatsInJS.ph
 *
 * Returns an object that contains the count, sum,
 * minimum, median, maximum, mean, variance, and
 * standard deviation of the series of numbers stored
 * in the specified array.  This function changes the
 * specified array by sorting its contents.
 */

function Stats(data) {
    this.count = data.length;
    data.sort(ascend);

    /* Since the data is now sorted, the minimum value
     * is at the beginning of the array, the median
     * value is in the middle of the array, and the
     * maximum value is at the end of the array. */
    this.min = data[0];
    var middle = Math.floor(data.length / 2);
    if ((data.length % 2) != 0) {
        this.median = data[middle];
    }
    else {
        this.median = (data[middle - 1] + data[middle]) / 2;
    }
    this.max = data[data.length - 1];

    /* Compute the mean and variance using a
     * numerically stable algorithm. */
    var sqsum = 0;
    this.mean = data[0];
    for (var i = 1; i < data.length; ++i) {
        var x = data[i];
        var delta = x - this.mean;
        var sweep = i + 1.0;
        this.mean += delta / sweep;
        sqsum += delta * delta * (i / sweep);
    }
    this.sum = this.mean * this.count;
    this.variance = sqsum / this.count;
    this.sdev = Math.sqrt(this.variance);
}

/** Compares two objects using
 * built-in JavaScript operators. */
function ascend(a, b) {
    if (a < b) return -1;
    else if (a > b) return 1;
    return 0;
}
